#!/usr/bin/env ruby
# RDoc frontend for RubyOSA. Generate API reference documentation for the
# given application, based on the descriptions in the sdef(5).
#
# Copyright (c) 2006-2007, Apple Inc. All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer. 
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution. 
# 3.  Neither the name of Apple Inc. ("Apple") nor the names of
#     its contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission. 
# 
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

require 'tmpdir'
require 'rbconfig'
require 'rbosa'

def usage
  STDERR.puts <<-EOS
Usage: #{$0} [--addition] [--name | --path | --bundle_id | --signature] <criterion> [rdoc-options...]
Examples:
    # Generate HTML documentation for iTunes:
    #{$0} --name iTunes
    # Generate RI documentation for iTunes:
    #{$0} --name iTunes --ri
    # Generate HTML documentation for the StandardAdditions scriptable addition:
    #{$0} --addition --name StandardAdditions
See rdoc --help for additional options.
EOS
  exit 1
end

def unique_tmp_path(base, extension='', dir=Dir.tmpdir)
  i = 0
  loop do
    p = File.join(dir, "#{base}-#{i}-#{Process.pid}" + extension)
    return p unless File.exists?(p)
    i += 1
  end
end

usage unless ARGV.length >= 2 
addition = key = criterion = nil
while arg = ARGV.shift
  case arg
  when '--addition'
    addition = true
  when '--name', '--path', '--bundle_id', '--signature'
    if key
      $stderr.puts "You cannot use --name, --path, --bundle_id or --signature more than once."
      exit 1
    end
    key = arg[2..-1].intern
    criterion = ARGV.shift
    usage if criterion.nil?
  else
    if key and criterion
      ARGV.unshift(arg)
      break
    end
    usage
  end
end

DOC_NOT_AVAILABLE = 'Documentation not available.'

app = app_name = nil

if addition
  app_name = criterion if key == :name
  mod = Module.new
  OSA.const_set('TheApplication', mod)
  klass = Class.new(OSA::Element)
  mod.const_set('Application', klass) 
  klass.class_eval do
    include OSA::EventDispatcher
    METHODS_DESCRIPTION = []
    DESCRIPTION = 'The application class.'
  end
  app = klass.new.merge(key => criterion)
else
  app = OSA.app(key => criterion)
  app_name = if app.respond_to?(:name)
    app.name
  else
    if key != :name
      STDERR.puts "Can't guess the application name, because the application doesn't have a #name method. Please use `--name' instead."
      exit 1
    else
      criterion
    end
  end
end

mod = OSA.const_get(app.class.name.scan(/^OSA::(.+)::Application$/).to_s)
fake_ruby_src = mod.constants.map do |const_name|
  obj = mod.const_get(const_name)
  case obj
  when Class
    # Class.
    methods_desc = obj.const_get('METHODS_DESCRIPTION').map do |method|
      args_doc, args_def, args_def_opt = '', '', ''
      if method.args and !method.args.empty?
        args_doc_ary, args_def_ary, args_def_opt_ary = [], [], []
        method.args.each do |x| 
          arg = x.name
          desc = x.description
          desc = DOC_NOT_AVAILABLE if desc.empty?
          args_doc_ary << "  # #{arg}::\n  #  #{desc}" + (x.optional? ? ' Optional. Can be passed as a Hash key/value.' : '')
          if x.optional?
            args_def_ary << x.name + '=nil'
            args_def_opt_ary << ':' + x.name + ' => nil'
          else
            args_def_ary << x.name
            args_def_opt_ary << x.name
          end
        end
        args_doc = args_doc_ary.join("\n")
        args_def = '(' + args_def_ary.join(', ') + ')'
        args_def_opt = '(' + args_def_opt_ary.join(', ') + ')'
      end
      if method.result
        args_doc << "\n" unless args_doc.empty?
        desc = method.result.description
        desc = DOC_NOT_AVAILABLE if desc.empty?
        args_doc << "  # Returns::\n  #  #{desc}\n"
      end
      <<EOS
  # call-seq:
  #  #{method.name + args_def}
  #  #{args_def_opt != args_def ? method.name + args_def_opt : ''}
  #
  # #{method.description}
#{args_doc}
  def #{method.name}#{args_def}; end
EOS
    end
    <<EOS
# #{(obj.const_get('DESCRIPTION') || 'n/a')}
class #{obj.name} < #{obj.superclass}
#{methods_desc.join.rstrip}
end

EOS
  when Module
    # Enumeration group.
    next unless obj.const_defined?(:DESCRIPTION)
    enums_desc = obj.const_get(:DESCRIPTION).map do |item|
      <<EOS
  # #{item.description}
  #{item.name} = '#{obj.const_get(item.name).code}'
EOS
    end
    <<EOS
module #{mod.name}::#{const_name}
#{enums_desc}
end

EOS
  end
end.
join

header = if addition
  <<EOS
# This documentation describes the RubyOSA API for the #{criterion} scriptable addition. It has been automatically generated.
#
# In order to use this API you have to merge the scriptable addition into an application object. For instance:
#
#   OSA.app('iTunes').merge(#{app_name ? "'#{app_name}'" : ":#{key} => '#{criterion}'"})
#
# The module OSA::TheApplication is fake, everything inside will be defined in the module of the application you are controlling (for iTunes, in OSA::ITunes).
EOS
else
  <<EOS
# This documentation describes the RubyOSA API for the #{app_name} application. It has been automatically generated.
#
# The main class is #{mod.name}::Application, of which an instance is created likewise:
#
#   OSA.app('#{app_name}')
EOS
end

header << <<EOS
#
# For more information about RubyOSA, please visit the project homepage: http://rubyosa.rubyforge.org.
module OSA; end
# The #{app_name} module.
module #{mod.name}; end
EOS

fake_ruby_src = header << fake_ruby_src

rdoc_flags = ''
datadir = if Config.respond_to?(:datadir)
  Config.datadir('rubyosa')
else
  File.join(Config::CONFIG['datadir'], 'rubyosa')
end
template = File.join(datadir, 'rdoc_html.rb')
if File.exists?(template)
  rdoc_flags << " --template '#{template}' "
end
rdoc_flags << " --title '#{app_name} RubyOSA API' "
rdoc_flags << ' --main OSA '
rdoc_flags << ARGV.join(' ')

path = unique_tmp_path(app_name, '.rb')
File.open(path, 'w') { |io| io.puts fake_ruby_src }
line = "rdoc #{rdoc_flags} \"#{path}\""
unless system(line)
  STDERR.puts "Error when executing `#{line}' : #{$?}"
  exit 1
end
File.unlink(path)
